package fr.upmc.dtgui.reflexion;

import java.lang.reflect.Modifier;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtConstructor;
import javassist.CtField;
import javassist.CtMethod;

/**
 * <b>DataThreadFactory est la classe chargée de créer les Threads d'envoi et
 * de réception de donnée de la station de téléopération.</b>
 * <p>
 * Cette classe permet à la station de téléopération d'avoir deux Threads pour
 * lire et écrire son état.
 * </p>
 * 
 * @see fr.upmc.dtgui.reflexion.TeleoperationBoardFactory
 * 
 * @author Ayoub Benali, Maxime Sarilar
 */

public class DataThreadFactory {
	
	/**
     * La méthode makeActuatorDataSender.
     * <p>
     * Cette méthode se charge de créer le Thread qui va permettre à la station
     * de téléopération d'envoyer des ordres sour la forme de structure de 
     * données au robot pour qu'il change sa vitesse, son steering.
     * </p>
     * 
     * @param pool
     *            Le ClassPool associé à la classe chargée.
     * @param board
     *            La CtClass de la station de téléopération.
     * @return
     * 			  La CtClass ActuatorDataReceptor générée.
     * @see java.lang.Thread
     * @see fr.upmc.dtgui.robot.RobotActuatorCommand
     */
	public static CtClass makeActuatorDataSender(CtClass board, ClassPool pool)
			throws Exception {
		CtClass actuatorDataSender = board.makeNestedClass(
				"ActuatorDataSender", true);
		actuatorDataSender.setSuperclass(pool.getCtClass("java.lang.Thread"));
		
		//On va utiliser l'interface fournie:
		actuatorDataSender.addInterface(pool
				.getCtClass("fr.upmc.dtgui.gui.ActuatorDataSenderInterface"));

		//Création des champs:
		CtField rac = new CtField(
				pool.getCtClass("fr.upmc.dtgui.robot.RobotActuatorCommand"),
				"rac", actuatorDataSender);
		CtField commandQueue = new CtField(
				pool.getCtClass("java.util.concurrent.BlockingQueue"),
				"commandQueue", actuatorDataSender);
		actuatorDataSender.addField(rac);
		actuatorDataSender.addField(commandQueue);

		//Le constructeur:
		CtConstructor actuatorDataSenderConstructor = new CtConstructor(
				new CtClass[] {
					pool.getCtClass("fr.upmc.dtgui.robot.RobotActuatorCommand"),
						pool.getCtClass("java.util.concurrent.BlockingQueue") },
				actuatorDataSender);
		actuatorDataSenderConstructor
				.setBody("{super();$0.rac = $1; $0.commandQueue = $2;}");
		actuatorDataSender.addConstructor(actuatorDataSenderConstructor);

		/* On va créer un Runnable qui sera utilisé dans le run() du Thread, 
		 * car Javassist n'aime pas les classes anonymes.
		 */
		CtClass adst = createAdsThread(board, pool);

		//La méthode run(), inhérente à tout Thread:
		CtMethod run = new CtMethod(CtClass.voidType, "run", new CtClass[] {},
				actuatorDataSender);
		String runBody = "{" + "try {"
				+ "javax.swing.SwingUtilities.invokeAndWait(" + "new "
				+ adst.getName() + "(rac, commandQueue)" + ") ;"
				+ "} catch (InterruptedException e1) {"
				+ "e1.printStackTrace();"
				+ "} catch (java.lang.reflect.InvocationTargetException e1) {"
				+ "e1.printStackTrace();" + "}" + "}";
		run.setBody(runBody);
		actuatorDataSender.addMethod(run);
		
		/*
		 * On finalise la classe pour garantir qu'elle ne sera pas modifiée
		 * passé cette méthode:
		 */
		actuatorDataSender.toClass();
		return actuatorDataSender;
	}
	
	/**
     * La méthode makeSensorDataReceptor.
     * <p>
     * Cette méthode se charge de créer le Thread qui va permettre à la station
     * de téléopération de lire l'état du robot sour la forme de structure de 
     * données.
     * </p>
     * 
     * @param pool
     *            Le ClassPool associé à la classe chargée.
     * @param board
     *            La CtClass de la station de téléopération.
     * @return
     * 			  La CtClass SensorDataReceptor générée.
     * @see java.lang.Thread
     * @see fr.upmc.dtgui.robot.RobotStateData
     */
	public static CtClass makeSensorDataReceptor(CtClass board, ClassPool pool)
			throws Exception {
		CtClass sensorDataReceptor = board.makeNestedClass(
				"SensorDataReceptor", true);
		sensorDataReceptor.setModifiers(Modifier.PUBLIC);
		sensorDataReceptor.setSuperclass(pool.getCtClass("java.lang.Thread"));
		
		//On va utiliser l'interface fournie:
		sensorDataReceptor.addInterface(pool
				.getCtClass("fr.upmc.dtgui.gui.SensorDataReceptorInterface"));

		//Création des champs:
		CtField positionDisplay = new CtField(
				pool.getCtClass("fr.upmc.dtgui.gui.PositionDisplay"),
				"positionDisplay", sensorDataReceptor);
		CtField tBoard = new CtField(board, "tBoard", sensorDataReceptor);
		CtField dataQueue = new CtField(
				pool.getCtClass("java.util.concurrent.BlockingQueue"),
				"dataQueue", sensorDataReceptor);
		CtField absoluteX = new CtField(CtClass.intType, "absoluteX",
				sensorDataReceptor);
		CtField absoluteY = new CtField(CtClass.intType, "absoluteY",
				sensorDataReceptor);
		CtField controlRadius = new CtField(CtClass.intType, "controlRadius",
				sensorDataReceptor);
		CtField shouldContinue = new CtField(CtClass.booleanType,
				"shouldContinue", sensorDataReceptor);
		CtField rsd1 = new CtField(
				pool.get("fr.upmc.dtgui.robot.RobotStateData"), "rsd1",
				sensorDataReceptor);
		sensorDataReceptor.addField(rsd1);
		CtField pd = new CtField(
				pool.get("fr.upmc.dtgui.robot.PositioningData"), "pd",
				sensorDataReceptor);
		sensorDataReceptor.addField(pd);

		sensorDataReceptor.addField(positionDisplay);
		sensorDataReceptor.addField(tBoard);
		sensorDataReceptor.addField(dataQueue);
		sensorDataReceptor.addField(absoluteX);
		sensorDataReceptor.addField(absoluteY);
		sensorDataReceptor.addField(controlRadius);
		sensorDataReceptor.addField(shouldContinue);
		
		//Le constructeur:
		CtConstructor sensorDataReceptorConstructor = new CtConstructor(
				new CtClass[] {
						pool.getCtClass("fr.upmc.dtgui.gui.PositionDisplay"),
						pool.getCtClass("java.util.concurrent.BlockingQueue"),
						CtClass.intType, CtClass.intType, CtClass.intType },
				sensorDataReceptor);
		String sensorDataReceptorConstructorBody = "{" + "super();"
				+ "$0.positionDisplay = $1;" + "$0.dataQueue = $2;"
				+ "$0.absoluteX = $3 ;" + "$0.absoluteY = $4 ;"
				+ "$0.controlRadius = $5 ;" + "}";
		sensorDataReceptorConstructor
				.setBody(sensorDataReceptorConstructorBody);
		sensorDataReceptor.addConstructor(sensorDataReceptorConstructor);

		//Ajout des méthodes:
		CtMethod cutoff = new CtMethod(CtClass.voidType, "cutoff",
				new CtClass[] {}, sensorDataReceptor);
		String cutoffBody = "{" + "$0.shouldContinue = false;" + "}";
		cutoff.setBody(cutoffBody);
		sensorDataReceptor.addMethod(cutoff);
		CtMethod setTBoard = new CtMethod(
				CtClass.voidType,
				"setTBoard",
				new CtClass[] { pool
					.getCtClass("fr.upmc.dtgui.gui.RobotTeleoperationBoard")},
				sensorDataReceptor);
		String setTBoardBody = "{" + "$0.tBoard = $1 ;" + "}";
		setTBoard.setBody(setTBoardBody);
		sensorDataReceptor.addMethod(setTBoard);

		CtMethod start = new CtMethod(CtClass.voidType, "start",
				new CtClass[] {}, sensorDataReceptor);
		String startBody = "{" + "$0.shouldContinue = true;" + "super.start();"
				+ "}";
		start.setBody(startBody);
		sensorDataReceptor.addMethod(start);

		/* On va créer deux Runnable qui seront utilisés dans le run() du Thread, 
		 * car Javassist n'aime pas les classes anonymes.
		 */
		CtClass thread1 = createSdrThread1(board, pool, sensorDataReceptor);
		CtClass thread2 = createSdrThread2(board, pool, sensorDataReceptor);

		//La méthode run(), inhérente à tout Thread:
		CtMethod run = new CtMethod(CtClass.voidType, "run", new CtClass[] {},
				sensorDataReceptor);
		String runBody = "{"
				+ "fr.upmc.dtgui.robot.RobotStateData rsd = null ;"
				+ "java.util.Vector current = new java.util.Vector(4) ;"
				+ "while ($0.shouldContinue) {"
				+ "try {"
				+ "	rsd = $0.dataQueue.take() ;"
				+ "} catch (InterruptedException e) {" + "e.printStackTrace();"
				+ "}" + "current.add(rsd) ;"
				+ "int n = $0.dataQueue.drainTo(current) ;	"
				+ "for (int i = 0 ; i <= n ; i++) {"
				+ "rsd = current.elementAt(i) ;" + "try {"
				+ "if (rsd instanceof fr.upmc.dtgui.robot.PositioningData) {"
				+ "$0.pd = rsd ;"
				+ thread1.getName()
				+ " t1 = new "
				+ thread1.getName()
				+ "($0);"
				+ "javax.swing.SwingUtilities.invokeAndWait("
				+ "t1"
				+ ") ;"
				+ "} else {"
				+ "if ($0.tBoard != null) {"
				+ "$0.rsd1 = rsd ;"
				+ thread2.getName()
				+ " t2 = new "
				+ thread2.getName()
				+ "($0);"
				+ "javax.swing.SwingUtilities.invokeAndWait("
				+ "t2"
				+ ") ;"
				+ "}"
				+ "}"
				+ "} catch (java.lang.InterruptedException e) {"
				+ "e.printStackTrace();"
				+ "} catch (java.lang.reflect.InvocationTargetException e) {"
				+ "e.printStackTrace();"
				+ "}"
				+ "}"
				+ "current.clear() ;"
				+ "}" + "}";
		run.setBody(runBody);
		sensorDataReceptor.addMethod(run);
		
		/*
		 * On finalise la classe pour garantir qu'elle ne sera pas modifiée
		 * passé cette méthode:
		 */
		sensorDataReceptor.toClass();
		return sensorDataReceptor;
	}
	
	/**
     * La méthode createAdsThread.
     * <p>
     * Cette méthode se charge de créer le Runnable qui sera utilisé dans le
     * run() de l'ActuatorDataSender (ads).
     * </p>
     * 
     * @param pool
     *            Le ClassPool associé à la classe chargée.
     * @param board
     *            La CtClass de la station de téléopération.
     * @return
     * 			  La CtClass adst générée.
     * @see java.lang.Thread
     * @see fr.upmc.dtgui.robot.RobotActuatorCommand
     */
	private static CtClass createAdsThread(CtClass board, ClassPool pool)
			throws Exception {
		CtClass adst = board.makeNestedClass("adst", true);
		adst.addInterface(pool.getCtClass("java.lang.Runnable"));

		//Création des champs:
		CtField commandQueue = new CtField(
				pool.getCtClass("java.util.concurrent.BlockingQueue"),
				"commandQueue", adst);
		CtField rac = new CtField(
				pool.getCtClass("fr.upmc.dtgui.robot.RobotActuatorCommand"),
				"rac", adst);

		adst.addField(commandQueue);
		adst.addField(rac);

		//Le constructeur:
		CtConstructor adstConstructor = new CtConstructor(new CtClass[] {
				pool.getCtClass("fr.upmc.dtgui.robot.RobotActuatorCommand"),
				pool.getCtClass("java.util.concurrent.BlockingQueue") }, adst);
		String adstConstructorBody = "{$0.rac = $1; $0.commandQueue = $2;}";
		adstConstructor.setBody(adstConstructorBody);

		//La méthode run() inhérente à tout Runnable:
		CtMethod run = new CtMethod(CtClass.voidType, "run", new CtClass[] {},
				adst);
		String runBody = "{$0.commandQueue.clear(); " +
				"$0.commandQueue.add($0.rac);}";
		run.setBody(runBody);

		adst.addConstructor(adstConstructor);
		adst.addMethod(run);
		
		/*
		 * On finalise la classe pour garantir qu'elle ne sera pas modifiée
		 * passé cette méthode:
		 */
		adst.toClass();
		return adst;
	}
	
	/**
     * La méthode createSdrThread1.
     * <p>
     * Cette méthode se charge de créer le premier Runnable qui sera utilisé
     * dans le run() du SensorDataReceptor (sdr).
     * </p>
     * 
     * @param pool
     *            Le ClassPool associé à la classe chargée.
     * @param board
     *            La CtClass de la station de téléopération.
     * @param sensorDataReceptor
     *            La CtClass du Thread SensorDataReceptor.
     * @return
     * 			  La CtClass sdrT1 générée.
     * @see java.lang.Runnable
     * @see fr.upmc.dtgui.robot.RobotActuatorCommand
     */
	private static CtClass createSdrThread1(CtClass board, ClassPool pool,
			CtClass sensorDataReceptor) throws Exception {
		CtClass sdrT1 = sensorDataReceptor.makeNestedClass("sdrT1", true);
		sdrT1.addInterface(pool.getCtClass("java.lang.Runnable"));

		/* Initialement, on utilisait dans ce Runnable uniquement les champs
		 * nécessaires à son exécution. Or donc, il y avait une erreur à
		 * l'exécution pour une raison inconnue. 
		 * 
		 * En testant diverses manières de contourner le problème 
		 * (incompréhensible) à defaut de le corriger, on a trouvé une solution. 
		 * 
		 * Le Runnable contiendrait non pas le strict nécessaire, mais toute la 
		 * classe SensorDataReceptor. Ce Runnable se chargera de récuperer lors 
		 * de son exécution ce dont il a besoin.
		 */
		
		//Le champ évoqué plus haut:
		CtField sdr = new CtField(sensorDataReceptor, "sdr", sdrT1);
		sdrT1.addField(sdr);
		
		//Le constructeur, qui permet d'initialiser le champ:
		CtConstructor sdrT1Constructor = new CtConstructor(
				new CtClass[] { sensorDataReceptor }, sdrT1);
		String sdrT1ConstructorBody = "{" + "$0.sdr = $1;" + "}";
		sdrT1Constructor.setBody(sdrT1ConstructorBody);
		sdrT1.addConstructor(sdrT1Constructor);

		//Et enfin le run(), qui va extraire les données pertinentes du champ:
		CtMethod run = new CtMethod(CtClass.voidType, "run", new CtClass[] {},
				sdrT1);
		String runBody = "{$0.sdr.positionDisplay.draw(" +
				"(fr.upmc.dtgui.robot.PositioningData)$0.sdr.pd);}";
		run.setBody(runBody);
		run.setModifiers(Modifier.PUBLIC);
		sdrT1.addMethod(run);
		
		/*
		 * On finalise la classe pour garantir qu'elle ne sera pas modifiée
		 * passé cette méthode:
		 */
		sdrT1.toClass();
		return sdrT1;
	}
	/**
     * La méthode createSdrThread2.
     * <p>
     * Cette méthode se charge de créer le second Runnable qui sera utilisé
     * dans le run() du SensorDataReceptor (sdr).
     * </p>
     * 
     * @param pool
     *            Le ClassPool associé à la classe chargée.
     * @param board
     *            La CtClass de la station de téléopération.
     * @param sensorDataReceptor
     *            La CtClass du Thread SensorDataReceptor.
     * @return
     * 			  La CtClass sdrT2 générée.
     * @see java.lang.Runnable
     * @see fr.upmc.dtgui.robot.RobotActuatorCommand
     */
	private static CtClass createSdrThread2(CtClass board, ClassPool pool,
			CtClass sensorDataReceptor) throws Exception {
		CtClass sdrT2 = sensorDataReceptor.makeNestedClass("sdrT2", true);
		sdrT2.addInterface(pool.getCtClass("java.lang.Runnable"));

		/* Initialement, on utilisait dans ce Runnable uniquement les champs
		 * nécessaires à son exécution. Or donc, il y avait une erreur à
		 * l'exécution pour une raison inconnue. 
		 * 
		 * En testant diverses manières de contourner le problème 
		 * (incompréhensible) à defaut de le corriger, on a trouvé une solution. 
		 * 
		 * Le Runnable contiendrait non pas le strict nécessaire, mais toute la 
		 * classe SensorDataReceptor. Ce Runnable se chargera de récuperer lors 
		 * de son exécution ce dont il a besoin.
		 */
		
		//Le champ évoqué plus haut:
		CtField sdr = new CtField(sensorDataReceptor, "sdr", sdrT2);
		sdrT2.addField(sdr);

		//Le constructeur, qui permet d'initialiser le champ:
		CtConstructor sdrT2Constructor = new CtConstructor(
				new CtClass[] { sensorDataReceptor }, sdrT2);
		String sdrT2ConstructorBody = "{" + "$0.sdr = $1;" + "}";
		sdrT2Constructor.setBody(sdrT2ConstructorBody);
		sdrT2.addConstructor(sdrT2Constructor);

		//Et enfin le run(), qui va extraire les données pertinentes du champ:
		CtMethod run = new CtMethod(CtClass.voidType, "run", new CtClass[] {},
				sdrT2);
		String runBody = "{if ($0.sdr.tBoard != null) " +
				"{ $0.sdr.tBoard.processSensorData(" +
				"(fr.upmc.dtgui.robot.RobotStateData)$0.sdr.rsd1) ; }}";
		run.setBody(runBody);
		run.setModifiers(Modifier.PUBLIC);
		sdrT2.addMethod(run);

		/*
		 * On finalise la classe pour garantir qu'elle ne sera pas modifiée
		 * passé cette méthode:
		 */
		sdrT2.toClass();
		return sdrT2;
	}
}
